package particle_simulator_chem;

import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.swt.graphics.GC;

import renderer.Camera;

import chemistry.Compound;
import chemistry.ElementList;
import chemistry.Reactor;

import auxiliary_classes.Vector4;

public class World extends Thread {
	private int width;
	private int height;
	private int depth;
	
	private double gravity;
	private double gravX;
	private double gravY;
	private double gravZ;
	private double defaultGravity;
	private double defaultGX;
	private double defaultGY;
	private double defaultGZ;
	
	private double airFriction;
	private ArrayList<Boid> boids;
	private int illustration_shape = 8;//0 is point, 1 is circle, 2 is text, 3 is int coords, 4 is double coords, 5-7 are for 3D debugging
	private int illustration_radius = 3;
	private ArrayList<Compound> compounds = new ArrayList<Compound>(0);
	private ElementList elements;
	private Reactor r;
	private String current_formula = "Fe"; //defaults to iron
	private int sleep = 0;
	private int default_sleep = 0;
	private int current_tool = 0; //defaults to 'boid blower'
	private static Map<Integer, String> unicodeMap = new HashMap<Integer, String>(9);
	
	//2D constructor
	public World(int width, int height, double gravity, double gravX, double gravY, double airFriction, ArrayList<Compound> compounds) throws FileNotFoundException{
		this.width = width;
		this.height = height;
		this.gravity = gravity;
		this.gravX = gravX;
		this.gravY = gravY;
		this.defaultGravity = gravity;
		this.defaultGX = gravX;
		this.defaultGY = gravY;
		this.airFriction = airFriction;
		this.setBoids(new ArrayList<Boid>(0));
		this.elements = new ElementList();
		this.r = new Reactor(elements);
		this.compounds = compounds;
		populateUnicodeMap(World.unicodeMap);
	}

	//3D constructor
	public World(int width, int height, int depth, double gravity, double gravX, double gravY, double gravZ, double airFriction, ArrayList<Compound> compounds) throws FileNotFoundException{
		this.width = width;
		this.height = height;
		this.depth = depth;
		this.gravity = gravity;
		this.gravX = gravX;
		this.gravY = gravY;
		this.gravZ = gravZ;
		this.defaultGravity = gravity;
		this.defaultGX = gravX;
		this.defaultGY = gravY;
		this.defaultGZ = gravZ;
		this.airFriction = airFriction;
		this.setBoids(new ArrayList<Boid>(0));
		this.elements = new ElementList();
		this.r = new Reactor(elements);
		this.compounds = compounds;
		populateUnicodeMap(World.unicodeMap);
	}
	
//////////////////////////////////Boid Adding///////////////////////////////////////
	
	public void addBoid(Boid boid) {
		this.getBoids().add(boid);
	}
	
//////////////////////////////////Display///////////////////////////////////////////
	
	public static void populateUnicodeMap(Map<Integer, String> unicodeMap2) {
		unicodeMap2.put(49, "\u2081");
		unicodeMap2.put(50, "\u2082");
		unicodeMap2.put(51, "\u2083");
		unicodeMap2.put(52, "\u2084");
		unicodeMap2.put(53, "\u2085");
		unicodeMap2.put(54, "\u2086");
		unicodeMap2.put(55, "\u2087");
		unicodeMap2.put(56, "\u2088");
		unicodeMap2.put(57, "\u2089");
	}
	
	
	//TODO: Add a field to Boid with unicode subscript instead of calculating it every time
	public static String unicodeSubscript(String ascii) {
		String unicode = "";
		for(int i = 0; i < ascii.length(); i++) {
			//System.out.println(i);
			int charInt = ascii.charAt(i);
			//System.out.println(charInt);
			if(charInt >= 49 && charInt <= 57) { //if the character is between 1 and 9
				unicode = unicode + (unicodeMap.get(charInt));
				//System.out.println(unicodeMap.get(charInt));
			} else {
				unicode = unicode + (Character.toString(ascii.charAt(i))); //String.concat(String) was not working
			}
		}
		//System.out.println("\u2081");
		return unicode;
	}
	
	public void drawBoid(Boid boid, GC gc, Camera camera) {
		double tempX = boid.getLocation().getX();
		double tempY = boid.getLocation().getY();
		if (getIllustrationShape() == 0)
			gc.drawPoint((int)(boid.getLocation().getX()), (int)(boid.getLocation().getY()));
		else if (getIllustrationShape() == 1)
			gc.fillOval((int)(boid.getLocation().getX()), (int)(boid.getLocation().getY()), getIllustrationRadius(), getIllustrationRadius());
		else if (getIllustrationShape() == 2)
			//gc.setFont(new Font()); //see API, implement in morning. Oh and look at the Fond Data method that gets the height... that could be really nice
			gc.drawText(boid.getText(), (int)(boid.getLocation().getX()), (int)(boid.getLocation().getY()), true);
		else if (getIllustrationShape() == 3)
			gc.drawText(boid.getLocation().roundToString(), (int)(tempX/boid.getLocation().getZ()), (int)(tempY/boid.getLocation().getZ()), true);
		else if (getIllustrationShape() == 4)
			gc.drawText(boid.getLocation().toString(), (int)(tempX/boid.getLocation().getZ()), (int)(tempY/boid.getLocation().getZ()), true);
		else if (getIllustrationShape() == 5)
			gc.fillOval((int)(tempX/boid.getLocation().getZ()) - (getIllustrationRadius() / 2), (int)(tempY/boid.getLocation().getZ()) - (getIllustrationRadius() / 2), (int) (boid.getZ() / 25) + (getIllustrationRadius() / 2), (int) (boid.getZ() / 25) + (getIllustrationRadius() / 2));
		else if (getIllustrationShape() == 6)
			gc.drawPoint((int)(tempX/boid.getLocation().getZ()), (int)(tempY/boid.getLocation().getZ()));
		else if (getIllustrationShape() == 7)
			gc.fillOval((int)(tempX/boid.getLocation().getZ()) + this.width / 2 - (getIllustrationRadius() / 2), (int)(tempY/boid.getLocation().getZ()) + this.height / 2 - (getIllustrationRadius() / 2), getIllustrationRadius(), getIllustrationRadius());
		else if (getIllustrationShape() == 8) {
			double[] pointToDraw = Camera.adjustPoint(camera.convertPoint(boid.getLocation()), 500, 500);
			gc.fillOval((int) pointToDraw[0], (int) pointToDraw[1], (int) (500 / pointToDraw[2]), (int) (500 / pointToDraw[2]));
		}
		
	}
	
	public void drawAllBoids(GC gc, Camera camera) {
		for(Boid boid : this.getBoids()) {
			gc.setForeground(boid.getColor());
			if(illustration_shape != 2) //text is implemented with a rectangular background and the text on the foreground. Thus, if the background and foreground are the same, a rectangle is displayed.
				gc.setBackground(boid.getColor());
			drawBoid(boid, gc, camera);
		}
	}
	
//////////////////////////////////Global Boid Updaters//////////////////////////////
	
	/**
	 * Checks if a boid is outside the world's bounds on each axis, and flips its velocity on that axis if it is.
	 * 
	 * @param Boid boid
	 */
	public void confineBoid(Boid boid) {
		double boidX = boid.getLocation().getX();
		double boidY = boid.getLocation().getY();
		double boidZ = boid.getLocation().getZ();
		if (boidX < 0) {
			boid.getLocation().setX(0);
			boid.setXVel(-boid.getXVel());
		} else if (boidX > this.width) {
			boid.getLocation().setX(this.width);
			boid.setXVel(-boid.getXVel());
		}
		
		if (boidY > this.height) {
			boid.getLocation().setY(this.height);
			boid.setYVel(-boid.getYVel());
		} else if (boidY < 0) {
			boid.getLocation().setY(0);
			boid.setYVel(-boid.getYVel()); 
		}
		
		 //boidZ will not be null if the boid is in 3D
		if ((Double) boidZ != null) { //primitive types cannot be null, so boidZ must be cast to Double, which is a wrapper for double
			if (boidZ < 0) {
				boid.getLocation().setZ(0);
				boid.setZVel(-boid.getZVel());
			} else if (boidZ > this.depth) {
				boid.getLocation().setZ(this.depth);
				boid.setZVel(-boid.getZVel());
			}
		}
	}
	
	public void gravitateBoid(Boid boid, double gravX, double gravY, double gravZ) {
		Vector4 gravity_loc = new Vector4(gravX, gravY, gravZ); //having an unused z won't detract from performance
		double[] angleToGravity = boid.angleTo(gravity_loc);
		//cos derp * hyp = the other one
		boid.setXVel((boid.getXVel() + (Math.cos(angleToGravity[0]) * this.gravity)));
		boid.setYVel((boid.getYVel() + (Math.sin(angleToGravity[0]) * this.gravity)));
		if ((Double) boid.getZ() != null) {
			boid.setZVel((boid.getZVel() + (Math.cos(angleToGravity[1]) + this.gravity)));
		}	
	}
	
	public void airFrictionizeBoid(Boid boid) {
		boid.setXVel(boid.getXVel() * (1 - this.airFriction));
		boid.setYVel(boid.getYVel() * (1 - this.airFriction));
		boid.setZVel(boid.getZVel() * (1 - this.airFriction));
	}
	
	public ArrayList<Boid> react(Boid a, Boid b) {
		ArrayList<String> productFormulae = this.r.react(a.getFormula(), b.getFormula());
		ArrayList<Boid> products = new ArrayList<Boid>();
		if (productFormulae != null) {
			for (String i: productFormulae) {
				Vector4 p = a.getLocation();
				products.add(new Boid(p.getX()+(int)((Math.random() -.5) * 10), p.getY()+(int)((Math.random() -.5) * 10), p.getZ()+(int)((Math.random() -.5) * 10),
							a.getXVel(), a.getYVel(), a.getZVel(), a.getColor(), i, i, elements));
			}
		} else {
			return null;
		}
		if(products != null) {
			for(Boid boid : products) {
				if (boid.getFormula().size() > 1) { //if boid is a compound 
					Compound compound = new Compound(boid.getText());
					if (!(containsCompound(this.compounds, compound))) {
						compounds.add(compound);
					}
				}
			}
		}
		return products;
	}
	
	public boolean containsCompound(ArrayList<Compound> compounds, Compound target) {
		for (Compound compound : compounds) {
			if (compound.getRawFormula().equals(target.getRawFormula()))
				return true;
		}
		return false;
	}
	
	public void updateAllBoids() {
		for(int i = 0; i < this.boids.size(); i++) {
			Boid boid = this.boids.get(i);
			this.gravitateBoid(boid, this.gravX, this.gravY, this.gravZ);
			this.airFrictionizeBoid(boid);
			ArrayList<Boid> neighbors = boidFilter(this.getBoids(), boid);
			Boid returned = boid.updateNeighborVelocities(neighbors);
			if (returned != boid) { //that is, if returned and boid are at the same memory address
				ArrayList<Boid> products = react(boid, returned);
				if (products != null) {
					for (Boid b:products) {
						this.boids.add(b);
					}
					this.boids.remove(boid);
					this.boids.remove(returned);
				}
			}
		}
		for(Boid boid : this.getBoids()) {
			boid.updatePosition();
			this.confineBoid(boid);
		}
	}

	
	public ArrayList<Boid> boidFilter(ArrayList<Boid> boids, Boid boid) { //I know this an ugly piece of shit, but it is java, which does not inherently have filter or lambda
		ArrayList<Boid> passed = new ArrayList<Boid>(0);
		for(Boid other_boid : boids) {
			if((boid.distanceTo(other_boid.getLocation()) <= boid.getUpdateRadius()) && !(boid == other_boid)) {
				passed.add(other_boid);
			}
		}
		return passed;
	}
	
	public void removeBoidsInRadius(Vector4 point, int radius) { //I know this an ugly piece of shit, but it is java, which does not inherently have filter or lambda
		for(int i = 0; i < this.getBoids().size(); i++) {//normal for loop instead of for each loop to avoid concurrent modification exception
			if((this.boids.get(i).distanceTo(point) <= radius)) {
				this.boids.remove(i);
			}
		}
	}
	
	public void resize(int width, int height) {
		this.width = width;
		this.height = height;
	}
	
	public int getNumBoids() {
		return this.getBoids().size();
	}
	
	public double getGravity() {
		return this.gravity;
	}

	public void setGravity(double gravity) {
		this.gravity = gravity;
	}

	public double getDefaultGravity() {
		return this.defaultGravity;
	}

	public void setDefaultGravity(double gravity) {
		this.defaultGravity = gravity;		
	}

	public void resetGravity() {
		this.gravity = this.defaultGravity;
		this.gravX = this.defaultGX;
		this.gravY = this.defaultGY;
		this.gravZ = this.defaultGZ;
	}

	/*public void setGravityLocation(double x, double y) {
		this.gravX = x;
		this.gravY = y;
	}*/

	public void setGravityLocation(double x, double y, double z) {
		this.gravX = x;
		this.gravY = y;
		this.gravZ = z;
	}

	public double getGravX() {
		return this.gravX;
	}
	
	public void setGravX(double gx) {
		this.gravX = gx;
	}

	public double getDefaultGX() {
		return this.defaultGX;
	}

	public void setDefaultGX(double gx) {
		this.defaultGX = gx;
	}

	public double getGravY() {
		return this.gravY;
	}
	
	
	public void setGravY(double gy) {
		this.gravY = gy;
	}

	public double getDefaultGY() {
		return this.defaultGY;
	}

	public void setDefaultGY(double gy) {
		this.defaultGY = gy;
	}

	public double getGravZ() {
		return this.gravZ;
	}

	public void setGravZ(double gravZ) {
		this.gravZ = gravZ;
	}

	public double getDefaultGZ() {
		return defaultGZ;
	}

	public void setDefaultGZ(double defaultGZ) {
		this.defaultGZ = defaultGZ;
	}

	public void setIllustrationRadius(int illustration_radius) {
		this.illustration_radius = illustration_radius;
	}

	public int getIllustrationRadius() {
		return illustration_radius;
	}

	public void setIllustrationShape(int illustration_shape) {
		this.illustration_shape = illustration_shape;
	}

	public int getIllustrationShape() {
		return illustration_shape;
	}
	
	public ElementList getElements() {
		return this.elements;
	}

	public void setCurrentFormula(String new_formula) {
		this.current_formula = new_formula;
	}
	
	public String getCurrentFormula() {
		return this.current_formula;
	}

	public void setSleep(int sleep) {
		this.sleep = sleep;
	}

	public int getSleep() {
		return sleep;
	}

	public void setDefaultSleep(int default_sleep) {
		this.default_sleep = default_sleep;
	}

	public int getDefaultSleep() {
		return default_sleep;
	}

	public double getAirFriction() {
		return this.airFriction;
	}
	
	public void setAirFriction(double af) {
		this.airFriction = af;
	}

	public void setBoids(ArrayList<Boid> boids) {
		this.boids = boids;
	}

	public ArrayList<Boid> getBoids() {
		return boids;
	}

	public void setCurrentTool(int current_tool) {
		this.current_tool = current_tool;
	}

	public int getCurrentTool() {
		return current_tool;
	}
	
	public ArrayList<Compound> getCompounds() {
		return this.compounds;
	}

	public void setWidth(int width) {
		this.width = width;
	}

	public int getWidth() {
		return width;
	}

	public void setHeight(int height) {
		this.height = height;
	}

	public int getHeight() {
		return height;
	}

	public void setDepth(int depth) {
		this.depth = depth;
	}

	public int getDepth() {
		return depth;
	}

	public boolean isPaused() {
		// TODO Auto-generated method stub
		return false;
	}
}
